Загрузка...
 
Печать

DirectX 8 Graphics. Meshes (полигональные сетки)


Вообще, ядро DirectX не работает с мешами, а только с полигонами. 1 Поддержку работы с мешами вновь "оставили на откуп" вспомогательной библиотеке D3DX (DirectX 8), которая предоставляет ряд объектов, предназначенных для хранения и рендеринга полигональных сеток (далее - мешей).
На самом низком уровне меши состоят из сотен (часто тысяч) вершин (vertices) и полигонов (polygons), каждые из которых требуют весьма сложных манипуляций. К счастью, в DirectX для хранения полигональных сеток предусмотрен свой собственный формат файлов с расширением .X, описывающий 3D-модель а также все её вершины, грани, нормали, текстуры.

Для компиляции примеров на понадобится:
  • MS Visual С++ 2010 Express,
  • Microsoft DirectX SDK 8.
Всё легко гуглится + есть в разделе "Софт" нашего сайта.

Содержание



Формат файлов .X

  • Это проприетарный формат от Майкрософт, предназначенный для хранения 3D-моделей.
  • Основан на шаблонах (templates), которые очень схожи с обычными структурами (structures) в языке С.
  • Расширяемый.
Т.е. можно добавлять для хранения свои собственные атрибуты.
  • Имеет много возможностей для хардкорных программеров.
.Х-файлы обычно создаются в специальных программах по работе с 3D-графикой (Milkshape3D, TrueSpace, 3DSMax) и могут сохранены/загружены как в двоичном, так и в текстовом форматах. Последний даёт возможность вносить корректировки с помощью любого текстового редактора.

Шаблоны (templates) .Х-файлов

В .Х-файлах шаблоны применяются для хранения массива данных. Обычно в шаблонах .Х-файла хранится информация:
  • о вершинах меша,
  • его полигонах,
  • текстурных картах (texture maps),
  • нормалях (normals).
Помимо DirectX-шаблонов, программер может создавать свои, пользовательские. Шаблоны могут объединяться в иерархию шаблонов (frame hierarchy). Например, шаблон, определяющий нормали вершин может быть включён в состав шаблона описания меша (mesh definition template). А тот, в свою очередь, часто входит в состав шаблона ключевого фрейма (frame of reference template; читай о нём ниже).
Игрокодер может создавать инстансы (instances) своих собственных шаблонов точно также, как это делается в C/C++. Техника "ссылки на шаблон" (template referencing) позволяет определить (define) в .Х-файле базовый шаблон и затем многократно использовать данные из него без необходимости переопределять другие шаблоны. Представь, что ты определил один меш, который хочешь использовать в рамках одного .Х-файла несколько раз (например: человек стоит, человек сидит, бежит и т.д.). Вместо того, чтобы вновь и вновь определять новые шаблоны, достаточно просто определить меш 1 раз и затем просто использовать ссылки на исходный шаблон.
Работа с большинством шаблонов .Х-файлов для игрокодера "прозрачна". При загрузке .Х-файла ему не придётся иметь с ними дело напрямую. Единственные исключения здесь:
  • шаблоны меша (mesh templates),
  • шаблоны фрейма (frame templates).

Иерархия фреймов (Frame Hierarchy)

Image
Рис. 1 Иерархия фреймов. Источник: http://netlib.narod.ru/library/book0051/ch02_12.htm

  • Лежит в основе применения продвинутых (advanced) мешей и их анимации.
  • Без неё никуда при использовании скинированных (skinned) мешей (об этом ниже).
  • Применяется для изоляции отдельных объектов сцены друг от друга.
То есть иерархия фреймов позволяет модифицировать (тонко настраивать) небольшую секцию сцены, указав отдельные фреймы. При этом остальные фреймы остаются нетронутыми. К примеру, если в твоей сцене есть фрейм дома (house frame) и двери (door frame), иерархия фреймов позволит работать с фреймом двери изолировано, не "беспокоя" фрейм дома.
Шаблон фрейма (frame tamplate), также известный как ключевой фрейм (frame of reference), используют для группировки одного или нескольких шаблонов (обычно это шаблоны меша) для более удобной работы с ними. Часто создают один (базовый) меш, после чего используют ещё несколько фреймов, содержащих ссылки на фрейм с базовым мешем. Это позволяет применять один и тот же меш несколько раз в пределах одного .Х-файла.
Возьмём, к примеру, меш бильярдного шара. Так как в наборе 15 шаров, мы создаём 15 фреймов, каждый из которых содержит ссылку на исходный меш шара. Далее каждый фрейм шара можно ориентировать в пространстве (путём применения шаблона матрицы трансформации фрейма (frame transformation matrix template)) на площади бильярдного стола. Это заставит каждый инстанс меша перемещаться по столу вместе с собственным фреймом. Таким образом из одного меша мы получим 15 бильярдных шаров.
Помимо возможности множить инстансы меша, фреймы также позволяют создавать иерархию фреймов (frame hierarchy). Именно она определяет (defines) структуру сцены или группы мешей.
К примеру, возьмём человеческий скелет. Представь, что он представляет собой набок фреймов (set of frames). На вершине иерархии находится грудная клетка (chest), к которой присоединены другие кости (см. Рис.1), к тем, в свою очередь, другие и т.д. Здесь фрейм грудной клетки является исходным (корневым фреймом, root frame). У корневого фрейма нет родительского фрейма. То есть он расположен в самом верху иерархического списка и не принадлежит никакому другому фрейму. Фреймы, присоединённые к другому фрейму (фреймам) называют потомками (child frames; nodes). Например предплечье (upper arm) является родителем (parent) для руки, а рука - родителем для ладони (hand). Предплечье, рука и ладонь являются потомками фрейма "грудная клетка".
При движении фрейма вместе с ним также движутся все его потомки. Это логично, ведь при движении предплечья присоединённые к нему рука и кисть (= ладонь) также двигаются вместе с ним. При движении одной только кисти руки, двигаться будет только она, т.к. у неё нет фреймов потомков (если, конечно, каждому пальцу не назначен свой фрейм).
У каждого фрейма есть своя отдельная ориентация в пространстве, которая в терминах .Х-формата называется трансформация фрейма (frame transformation). Трансформация любого дочернего фрейма (= фрейма-потомка) применяется (apply) к т.н. топ-трансформации (top transformation) т.е. трансформации фрейма-родителя (parent frame). Каждая трансформация транслируется вниз, от самого верхнего уровня иерархии до самого последнего фрейма-потомка (child frame). Например, когда ты вращаешь предплечьем (upper arm) трансформация вращения также действует вниз по иерархии, ко всем фреймам-потомкам (рука, кисть). При этом трансформация родителя комбинируется с трансформациями всех его потомков.

Создание .Х-мешей (Creating .X Meshes)

Наиболее популярный способ создания .Х-файлов - экспорт готовой модели из 3D-редактора. Для этого 3D-редактор должен поддерживать экспорт .Х-моделей, что встречается довольно редко. На помощь приходят плагины от сторонних разработчиков. В таблице ниже представлены наиболее популярные 3D-редакторы + кратко описан способ экспорта .X-файла:
НАЗВАНИЕ ОПИСАНИЕ ЗАГРУЗКА И УСТАНОВКА
3D Studio Max 7.0 Shareware Trial-версия самого популярного программного пакета для создания и редактирования 3D-моделей. Полностью работоспособная версия с 30-дневным ознакомительным периодом. По истечении 30 дней перестанет работать. На английском. Работает на любой ОС семейства Windows не старше XP. Берём здесь: http://www.ag.ru/files/software/11/11003(external link). Самораспаковывающийся архив весит прибл. 85 Мб. Распаковываем в любую папку на компьютере и запускаем Setup.exe . После установки запускаем приложение с помощью ярлыка на Рабочем столе. При запуске появляется наг-скрин с запросом на активацию программы. Выбираем "Remind me later" и жмём "Next". Для нормальной работы программы в MS Windows 7/8/10 x64 в свойствах ярлыка жми "Дополнительно" и отметь пункт "Запуск от имени администратора". 3DSMax 7 (как впрочем и многие другие версии) очень не любит видеоадаптеры, интегрированные в процессор. Сильно желательно комп с дискретной видеокартой. Пусть даже это будет ноутбук с GeForce 310.
Gmax Официально бесплатная (для некоммерческого использования) программа для создания и редактирования 3D-моделей. На английском. Работает на любой ОС семейства Windows не старше XP. Из минусов: в штатной поставке сохраняет и открывает только файлы внутреннего формата gmax. Отсутствует .x-экспортер. Тот, что ставится отдельно (MS Flight Simulator GamePack) экспортирует .x-файлы с лишними фреймами. Из-за этого они не открываются игровым приложением. Берём здесь: https://www.turbosquid.com/gmax(external link). После установки запускаем приложение с помощью ярлыка на Рабочем столе. При первом запуске запрашивает идентификатор пользователя. Ссылка для его получения устарела. Поэтому архив с необходимыми reg-файлами ищем в Гугле по запросу "Gmax v1.2 Registration Utility" либо забираем архив gmax_registration_workaround.zip отсюда: https://neverwintervault.org/project/nwn1/other/tool/gmax-12-registration-workaround(external link). После скачивания открываем архив и последовательно запускаем два файла с расширением .reg, согласившись на добавление информации из них в реестр вашей ОС.
Milkshape 3D Небольшой (6 Мб) 3D-редактор, умеющий экспортировать .X-файлы уже в базовой комплектации. Впрочем, не только их. На английском языке. Работает в любой ОС семейства MS Windows. Из минусов - нетривиальный интерфейс, к которому сложно привыкнуть. Берём здесь: http://www.milkshape3d.com(external link). Триальная версия работает не более 30 дней и потом просит зарегистрироваться (= заплатить). Прога хранит свои настройки в каталоге C:\Users\<Имя пользователя >\AppData\Roaming \MilkShape3D1.x.x\ (Win7/8/10).
Anim8or (Читается "анимэйтор".) Небольшой (4,7 Мб) бесплатный 3D-редактор от Steven Glanville (NVIDIA). На английском языке. Создаёт 3D-фигуры из стандартных примитивов. Работает в любой ОС семейства MS Windows. Позволяет экспортировать модели в форматы 3DS (устаревший формат файлов моделей 3DS Max версий 1-3), LWO (Lightwave 3D) и OBJ. Из минусов: нестандартный интерфейс, среди стандартных примитивов отсутствует плоскость (plane). Берём здесь: https://anim8or.com/index.html(external link). На офсайте программы переходим в раздел DOWNLOAD и скачиваем animv100.zip. Скачанный .zip-архив распаковать в любую папку на жёстком диске и запустить Anim8or.exe .
Wings 3D Бесплатный (для некоммерческого использования) 3D-редактор. На английском языке. Для работы требует видеокарту с поддержкой OpenGL 2.1 и выше. Берём здесь: http://www.wings3d.com(external link) . На офсайте программы переходим в раздел Downloads и скачиваем инсталлятор.
Blender 2.79 Бесплатный (для некоммерческого использования) мультиплатформенный 3D-редактор. На английском языке. Написан на Python. Одна из последних версий, работающая в Win7. Для работы требует видеокарту с поддержкой OpenGL 2.1 и выше. Поддерживает экспорт моделей в файлы самых разных форматов, включая DirectX (.x). Берём здесь: https://download.blender.org/release/Blender2.79(external link) (110 Мб). Скачивай инсталлятор или архив под разрядность своей ОС.
Blender 3 Бесплатный (для некоммерческого использования) мультиплатформенный 3D-редактор. На английском языке. Написан на Python. Для работы требует ОС Win8.1 и выше + видеокарту с поддержкой OpenGL 2.1 и выше. Поддерживает экспорт моделей в файлы самых разных форматов, включая DirectX (.x). Актуальную версию берём здесь: https://www.blender.org(external link) (210 Мб). На офсайте программы переходим в раздел Download и скачиваем инсталлятор под твою ОС.
Autodesk Softimage Mod Tool 7.5 Бесплатная (для некоммерческого использования) 32-битная версия легендарного 3D-редактора Softimage XSI ("Ксюша"; в ней, в частности, делали сцены с жидким металлом в х/ф "Терминатор 2"). На английском языке. Дружит с Win Vista/7/8/10. Для работы требует видеокарту с поддержкой OpenGL 2.1 и выше. Поддерживает экспорт моделей в файлы самых разных форматов, включая DirectX (.x). Дополнения с паками известных игр (Half-Life 2, Crysis) позволяют ещё больше расширить функционал. Многие (особенно в Японии) называли его лучшим 3D-редактором. С 2008 года проект закрыт и Mod Tool не обновляется. Берём здесь: https://www.moddb.com/downloads/autodesk-softimage-mod-tool-75(external link) (445 Мб).
MakeHuman 1.2.0 Бесплатный (опенсорсный) 3D-редактор человекоподобных персонажей. Дружит с Win 7/8/10. Для работы требует видеокарту с поддержкой OpenGL 2.1 и выше. Собственный формат файлов моделей MakeHuman умеет импортировать Blender. Тем не менее, Blender-импортер (+ ещё пару бонусов) на всякий случай заботливо положили в архив с программой. На сайте программы http://www.makehumancommunity.org(external link) в разделе "Assets" есть допстаф (модели людей, одежда, аксессуары и т.д.) Берём здесь: http://download.tuxfamily.org/makehuman/releases/makehuman-community-1.2.0-windows.zip(external link) (335 Мб).


Парсинг .X-файлов

  • Не так сложен, как может показаться на первый взгляд.
Суть данного процесса заключается в сканировании иерархии фреймов, в поисках требуемых шаблонов (обычно это шаблоны мешей и фреймов).
  • Проще выполнять, подключив вспомогательную библиотеку D3DX.
При "ручном" парсинге применяют семейство объектов IDirectXFile, которое открывает .X-файл и энумерирует все его шаблоны, оформляя их нужным образом для упрощения последующего доступа к ним. Самое сложное здесь - учесть, что шаблоны могут быть расположены внутри других шаблонов. Поэтому вместо искомого шаблона можно обнаружить ссылку (reference) на него, которую необходимо обработать для получения доступа к самому шаблону. Приведённый ниже фрагмент кода открывает .X-файл и парсит шаблоны внутри него:
Закрыть
noteПримечание

Для использования компонентов интерфейса IDirectXFile (DirectX 8) необходимо подключить к Проекту заголовочные файлы dxfile.h, rmxfguid.h, rmxftmpl.h и библиотеки dxguid.lib, d3dxof.lib .

...
BOOL ParseXFile(char *Filename)
{
	IDirectXFile	*pDXFile = NULL;
	IDirectXFileEnumObject	*pDXEnum = NULL;
	IDirectXFileData		*pDXData = NULL;

	// Создаём объект .X-файла
	if(FAILED(DirectXFileCreate(&pDXFile)))
		return FALSE;

	// Регистрируем нужные шаблоны.
	// Применяем стандартные шаблоны Direct3D (Retained Mode).
	if(FAILED(pDXFile->RegisterTemplates((LPVOID)D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES)))
	{
		pDXFile->Release();
		return FALSE;
	}

	// Создаём объект энумерации (enumeration object).
	if(FAILED(pDXFile->CreateEnumObject((LPVOID)Filename, DXFILELOAD_FROMFILE, &pDXEnum)))
	{
		pDXFile->Release();
		return FALSE;
	}

	// Ищем ключевые шаблоны (=шаблоны верхнего уровня; top-level templates).
	while(SUCCEEDED(pDXEnum->GetNextDataObject(&pDXData)))
	{
		ParseXFileData(pDXData);
		ReleaseCOM(pDXData);
	}

	// Освобождаем память, ранее выделенную под объекты.
	ReleaseCOM(pDXEnum);
	ReleaseCOM(pDXFile);

	// В случае успеха возвращаем TRUE,
	return TRUE;
}

void ParseXFileData(IDirectXFileData *pData)
{
	IDirectXFileObject *pSubObj = NULL;
	IDirectXFileData *pSubData = NULL;
	IDirectXFileDataReference *pDataRef = NULL;
	const GUID *pType = NULL;
	char *pName = NULL;
	DWORD dwSize;
	char *pBuffer;

	// Получаем тип шаблона.
	if(FAILED(pData->GetType(SpType)))
		return;

	// Получаем имя шаблона (если есть).
	if(FAILED(pData->GetName(NULL, SdwSize)))
		return;
	if(dwSize)
	{
		if((pName = new char[dwSize]) != NULL)
		{
			pData->GetName(pName, &dwSize);
		}
	}

	// Если шаблон не найден, присваиваем топ-шаблону имя по умолчанию.
	if(pName == NULL)
	{
		if((pName = new char[9]) == NULL)
			return;
		strcpy(pName, "Template");
	}

	// Смотрим, что за шаблон.
	// Сюда переходим из своего кода.
	
	// Сканируем в поисках вложенных (embedded) шаблонов.
	while(SUCCEEDED(pData->GetNextObject(&pSubObj)))
	{
		// Обрабатываем вложенные ссылки.
		if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileDataReference, (void**)&pDataRef)))
		{
			if(SUCCEEDED(pDataRef->Resolve(&pSubData)))
			{
				ParseXFileData(pSubData);
				ReleaseCOM(pSubData);
			}

			ReleaseCOM(pDataRef);
		}
		
		// Обрабатываем вложенные шаблоны без ссылок (non-referenced).
		if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileData, (void**)&pSubData)))
		{
			ParseXFileData(pSubData);
			ReleaseCOM(pSubData);
		}

		ReleaseCOM(pSubObj);
	}

	// Очищаем буфер с именем.
	delete pName;
}
...

В коде видим реализацию всего двух больших функций: ParseXFile и ParseXFileData. Они работают вместе, парся каждый шаблон, найденный в .Х-файле.
  • ParseXFile открывает .Х-файл и энумерирует (=сканирует) его в поиске самых верхних шаблонов (topmost templates) в иерархии.
Каждый найденный шаблон передаётся на обработку в функцию ParseXFileData.
  • ParseXFileData обрабатывает данные переданного шаблона. Её выполнение начинается с запроса типа шаблона и имени инстанса (если есть).
После этого переходит к обработке данных шаблона и с помощью рекурсии (=вызова самой себя) начинает энумерацию в поисках шаблонов-потомков (child templates). Этот процесс продолжается, пока все шаблоны не будут обработаны.
При вызове ParseXFile ей в качестве параметра передаётся имя загружаемого .X-файла. Далее две другие функции позаботятся обо всём остальном. При изучении темы "Скинированные меши" (Skinned meshs) они не раз нам помогут.

Работа с мешами с применением вспомогательной библиотеки d3dx.lib (DirectX 8)

Напомним, данная библиотека расположена в каталоге с установленным DirectX SDK 8. В нашем случае путь к ней такой: c:\DXSDK8\lib .
Вообще, в Direct3D все меши делятся на 2 вида:
Стандартный (standart; статичный, static) Не меняет свою геометрию при рендеринге. Для улучшения своего внешнего вида может использовать только текстурирование (texture-mapping).
Скинированный(skinned; анимированный, animated) Не меняет свою геометрию при рендеринге. Для улучшения своего внешнего вида может использовать только текстурирование (texture-mapping).Данный вид мешей уникален, т.к. способен менять свою форму в процессе рендеринга (=deformable). Для подготовки меша к деформации, как правило, (в 3D-редакторе) его вершины присоединяют (attach) к т.н. костям (bones; здесь служебные объекты, помогающие в анимировании меша). Всякий раз при движении костей присоединённые вершины меша двигаются вместе с ними.


Объект ID3DXBuffer (DirectX 8)

  • Специальный объект, который используют оба вида мешей для хранения своих данных.
  • Имеет всего две функции: GetBufferPointer и GetBufferSize.
Функция GetBufferPointer применяется для запроса указателя на данные буфера объекта. Возвращает указатель типа void, который может быть оттранслирован (cast) в любой тип данных:
void *ID3DXBuffer::GetBufferPointer();

Функция GetBufferSize возвращает число байт, задействованных под буфер объекта:
DWORD ID3DXBuffer::GetBufferSize ();

Библиотека D3DX использует объект ID3DXBuffer для хранения информации о конкретном меше. Например, это могут быть списки (lists) материалов и текстурных карт.
ID3DXBuffer создаётся путём вызова функции D3DXCreateBuffer:
Прототип функции D3DXCreateBuffer
HRESULT D3DXCreateBuffer
(
	DWORD NumBytes, // Размер создаваемого буфера.
	ID3DXBuffer **ppvBuffer // Указатель на объект буфера.
);

Вот пример, в котором создаётся объект буфера размером 1024 байт и заполняется нулями:
ID3DXBuffer *pBuffer;

// Создаём буфер
if(SUCCEEDED(D3DXCreateBuffer (1024, SpBuffer)))
{
	// Получаем указатель на буфер.
	char *pPtr = pBuffer->GetBufferPointer ();
	
	// Заполняем буфер нулями.
	memset(pPtr, 0, pBuffer->GetBufferSize ());
	
	// Освобождаем буфер.
	pBuffer->Release();
}


Стандартные меши (Standart Meshes; DirectX 8)

  • Содержит определение шаблона меша, заданного создателями DirectX.
  • Самый простой вид меша, с которого лучше всего начать работу.
Применение вспомогательной библиотеки D3DX делает работу со стандартными мешами ещё проще, т.к. заметно уменьшает объём кода для загрузки и рендеринга.
Для примера создадим объект меша ID3DXMesh (подробнее об этом см. ниже).
После создания инстанса ID3DXMesh вызываем функцию D3DXLoadMeshFromX, загружающую меш из .Х-файла. Вот её прототип:
Прототип функции D3DXLoadMeshFromX
HRESULT D3DXLoadMeshFromX(
	LPSTR pFilename, // Имя загружаемого файла .Х-файла
	DWORD Options, // Обычно здесь указывают D3DXMESH_SYSTEMMEM.
	IDirect3DDevice8 *pDevice, // Предварительно проинициализированный объект устройства Direct3D.
	ID3DXBuffer **Adjacency, // NULL
	ID3DXBuffer **pMaterials, // Указатель на готовый объект буфера ID3DXBuffer,
                                 //содержащий инфу по материалам меша.
	DWORD pNumMaterials, // Кол-во материалов у меша.
	ID3DXMesh **ppMesh // Указатель на создаваемый D3DX-o6ъект меша.
	);

Многие аргументы здесь библиотека D3DX заполняет автоматически. Программер предоставляет только следующие параметры:
  • имя .Х-файла;
  • указатель на готовый объект буфера ID3DXBuffer, содержащий инфу по материалам меша;
  • кол-во материалов меша;
  • указатель на создаваемый D3DX-объект меша.
При попытке загрузки с помощью функции D3DXLoadMeshFromX .Х-файла, содержащего несколько мешей, она (функция) автоматически объединит их в один меш. Часто это как раз то, что нужно.
Вот пример кода, загружающего меш:
...
// g_pD3DDevice - предварительно созданный и проинициализированный объект устройства Direct3D.
ID3DXBuffer *pD3DXMaterials;
DWORD g_dwNumMaterials; 
ID3DXMesh *g_pD3DXMesh;

if(FAILED(D3DXLoadMeshFromX("mesh.x", D3DXMESH_SYSTEMMEM, g_pD3DDevice,
	NULL, &pD3DXMaterials, &g_dwNumMaterials, &g_pD3DXMesh)))
{
	// Ошибка.
}
...

Сразу после (успешной) загрузки меша из .Х-файла запрашиваем инфу о его материалах (materials) и текстурных картах (texture-maps):
...
D3DXMATERIAL *pMaterials = NULL;
D3DMATERIAL8 *g_pMaterialList = NULL;
IDirect3DTexture8 **g_pTextureList;

// Получаем указатель на список материалов (material list).
pMaterials = (D3DXMATERIAL*)pD3DXMaterials->GetBufferPointer();

if(pMaterials != NULL)
{
	// Выделяем структуру для копирования туда данных.
	g_pMaterialList = new D3DMATERIAL8(dwNumMaterials);
	
	// Создаём массив указателей на объекты текстур (texture object pointers array).
	g_pTextureList = new IDirect3DTexture8(dwNumMaterials);
	
	// Копируем инфу по материалам меша.
	for (DWORD i=0; KdwNumMaterials; i++)
	{
		g_pMaterialList[i] = pMaterials[i].MatD3D;
		
		// Делаем цвет тени (ambient color) такой же, как рассеяный (diffuse color).
		g pMaterialList[i].Ambient = g pMaterialList[i].Diffuse;
		
		// Загружаем текстуры (если есть).
		if(FAILED(D3DXCreateTextureFromFile(g_pD3DDevice, g_pMaterials[i]->pTextureFilename,
			&g_pTextureList[i])))
		{
			g_pTextureList[i] = NULL;
		}
	}
	
	// Освобождаем буфер материалов по завершении их загрузки.
	pD3DXMaterials->Release ();
} else
{
	// Если материалы не загружены, создаём один материал по умолчанию.
	g_dwNumMaterials = 1;
	
	// Создаём белый (white) материал.
	g_pMaterialList = new D3DMATERIAL8[i];
	g pMaterialList[i].Diffuse.г = 1.0f;
	g_pMaterialList[i].Diffuse.g = 1.0f;
	g_pMaterialList[i].Diffuse.b = 1.0f;
	g_pMaterialList[i].Diffuse.a = 1.0f;
	g_pMaterialList[i].Ambient = g_pMaterialList[i].Diffuse;
	
	// Создаём пустую ссылку на текстуру.
	g pTextureList = new IDirect3DTexture8[i];
	g_pTextureList[0] = NULL;
}
...

После выполнения данного фрагмента у нас в распоряжении окажется список материалов и текстур, полностью готовый к применению в сцене. Следующий шаг - рендеринг меша.

Рендерим меши (Rendering Meshes; DirectX 8)

В "сердце" объекта ID3DXMesh лежит функция рендеринга DrawSubset. Субсетом (subset) называют часть (portion) меша, которая отделена, на случай изменения условий рендеринга. Самый типичный такой случай - смена материала или текстуры. Меш можно разбивать на множество субсетов. Здесь главное - помнить, что представляет собой каждый из них. Сразу после загрузки .Х-файла, ты остаёшься один на один с объектом меша и его материалами. Число субсетов совпадает с числом материалов. То есть, если объект меша содержит 5 материалов, меш содержит 5 субсетов, готовых к рендерингу. Такая зависимость упрощает процесс рендеринга меша. Мы просто сканируем каждый материал, назначаем его субсету и затем просто этот самый субсет рендерим. Весь процесс повторяется, пока весь меш не будет отрисован.
Для позиционирования меша в мировом пространстве (world space), до начала рендеринга назначаем мешу мировую матрицу трансформации (world transformation matrix).
Вот пример рендеринга меша, загруженного и подготовленного в предыдущих абзацах:
...
// g_pD3DDevice - предварительно созданный и проинициализированный объект устройства Direct3D.
// pD3DXmesh - готовый инстанс объекта ID3DXMesh.
// matWorld - мировая матрица трансформации меша.

// Начало сцены
if(SUCCEEDED(g_pD3DDevice->BeginScene()))
{
	// Назначаем мировую матрицу трансформации меша.
	g_pD3DDevice->SetTransform(D3DTS_WORLD, SmatWorld);
	
	// Проходим цикл по каждому материалу меша.
	for(DWORD i=0; i<g_dwNumMaterials; i++)
	{
		// Назначаем материал и текстуру.
		g_pD3DDevice->SetMaterial(&g_pMaterialList[i]);
		g_pD3DDevice->SetTexture(0, g_pTextureList[i]);
		
		// Отрисовываем субсет меша.
		pD3DXMesh->DrawSubset(i);
	}

	// Завершаем отрисовку сцены.
	g_pD3DDevice->EndScene();
}
...

Прежде чем рендерить меш, сперва необходимо установить (set) его мировую матрицу трансформации (world transformation matrix). Она необходима для корректного позиционирования меша в мировом пространстве. При загрузке нескольких мешей их можно объединить, создав анимированный объект путём простого изменения положения его частей в пространстве. Именно эта концепция лежит в основе real-time 3D-анимации.

Скинированные меши (Skinned Meshes)

  • Могут динамически изменяться во время рендеринга.
Это достигается путём:
  • объединения отдельных вершин в специальные структуры, "присоединяемые" к т.н. костям (bones);
  • перемещения отдельных фреймов иерархии.
Скинированный меш использует кости (bones) для определения (и сохранения в процессе анимации) своей формы. При движении костей, "насаженный" на них, меш перемещается вместе с ними.
Внутри .Х-файла кости представлены в виде иерархии фреймов (frame hierarchy). При моделировании меша фреймы объединяют по принципу родитель (parent) - потомок (child). При повороте родительского фрейма, все, присоединённые к нему, дочерние фреймы также наследуют трансформацию фрейма-родителя, которая, к тому же, комбинируется с их собственными трансформациями. Это заметно упрощает процесс анимирования для программера: двигаем родительский фрейм, а все дочерние двигаются вместе с ним.
Работу с анимированным мешем начинают с загрузки .Х-файла и парсинга его шаблонов (аналогично процессу описанному для стандартных мешей). Далее формируем список фреймов (list of frames) и иерархию фреймов.

Загрузка скинированных мешей (Load Skinned Meshes; DirectX 8)

Выше мы рассматривали процесс загрузки стандартного меша из .Х-файла. Там мы вызывали несколько функций из библиотеки D3DX для обработки шаблонов (templates). При загрузке скинированных мешей принцип тот же. Разве что применяется другая функция - D3DXLoadSkinMeshFromXof. В результате её выполнения считываются шаблоны из .Х-файла и на основе интерфейса ID3DXSkinMesh создаётся объект скинированного меша. Весь процесс проще представить, имея перед глазами исходный код примера. Поэтому создадим пример.

Пример приложения, показывающего меш (Win32, MSVC++2010, DirectX 8)

Создадим пример, который:
  • загружает .X файл,
  • грамотно его парсит,
  • "достаёт" шаблоны фреймов (frame templates; о них читай чуть ниже),
  • готовит текстуру и наносит её на 3D-модель.
Перед началом проверь, что у тебя установлены следующие программные компоненты:
  • MS Visual С++ 2010;
  • DirectX SDK 8.1
  • Windows SDK (для программирования под Win7/8/10).
Все эти штуки:
  • + инструкции по их установке ты найдёшь в разделе "Софт" нашего сайта;
  • Бесплатны;
  • Без труда гуглятся.

Создаём Проект приложения

  • Стартуй MS Visual C++ 2010, если не сделал этого раньше.
  • Создай пустой Проект с именем XFile01.
Проект автоматически разместится внутри Решения с таким же именем.
Весь процесс подробно расписан в статье MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта XFile01.
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Файл С++ (.срр)" и в поле "Имя" введи WinMain.cpp.
  • Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 6 XFile/Skinned Mesh Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)

Required libraries:
  WINMM.LIB, D3D8.LIB, D3DX8.LIB, and DXGUID.LIB
**************************************************/

// Macro to release COM objects
#define ReleaseCOM(x) if(x) { x->Release(); x = NULL; }

// Include files
#include <windows.h>
#include <stdio.h>
#include "d3d8.h"
#include "d3dx8.h"
#include "dxfile.h"
#include "rmxfguid.h"
#include "rmxftmpl.h"

// Window handles, class and caption text
// Дескриптор окна, класс и текст тулбара заданы в виде констант.
// Для удобства.
HWND          g_hWnd;
HINSTANCE     g_hInst;
static char   g_szClass[]   = "XFileClass";
static char   g_szCaption[] = "XFile Demo by Jim Adams";

// The Direct3D and Device object
IDirect3D8       *g_pD3D       = NULL;
IDirect3DDevice8 *g_pD3DDevice = NULL;

// A mesh definition structure
typedef struct sMesh
{
  char               *m_Name;            // Name of mesh

  ID3DXMesh          *m_Mesh;            // Mesh object
  ID3DXSkinMesh      *m_SkinMesh;        // Skin mesh object

  DWORD               m_NumMaterials;    // # materials in mesh
  D3DMATERIAL8       *m_Materials;       // Array of materials
  IDirect3DTexture8 **m_Textures;        // Array of textures

  DWORD               m_NumBones;        // # of bones
  ID3DXBuffer        *m_BoneNames;       // Names of bones
  ID3DXBuffer        *m_BoneTransforms;  // Internal transformations

  D3DXMATRIX         *m_BoneMatrices;    // X file bone matrices

  sMesh              *m_Next;            // Next mesh in list

  sMesh()
  { 
    m_Name           = NULL;  // Clear all structure data
    m_Mesh           = NULL;
    m_SkinMesh       = NULL;
    m_NumMaterials   = 0;
    m_Materials      = NULL;
    m_Textures       = NULL;
    m_NumBones       = 0;
    m_BoneNames      = NULL;
    m_BoneTransforms = NULL;

    m_Next           = NULL;
  }

  ~sMesh()
  {
    // Free all used resources
    delete [] m_Name;
    ReleaseCOM(m_Mesh);
    ReleaseCOM(m_SkinMesh);
    delete [] m_Materials;
    if(m_Textures != NULL) {
      for(DWORD i=0;i<m_NumMaterials;i++) {
        ReleaseCOM(m_Textures[i]);
      }
      delete [] m_Textures;
    }
    ReleaseCOM(m_BoneNames);
    ReleaseCOM(m_BoneTransforms);

    delete m_Next;  // Delete next mesh in list
  }
} sMesh;

// Structure to contain frame information
typedef struct sFrame
{
  char       *m_Name;      // Frame's name

  sMesh      *m_Mesh;      // Linked list of meshes

  sFrame     *m_Sibling;   // Sibling frame
  sFrame     *m_Child;     // Child frame

  sFrame() 
  {
    // Clear all data
    m_Name = NULL;
    m_Mesh = NULL;
    m_Sibling = m_Child = NULL;
  }

  ~sFrame()
  {
    // Delete all used resources, including linked list of frames
    delete m_Name;
    delete m_Mesh;
    delete m_Child;
    delete m_Sibling;
  }
} sFrame;

// Parent frame for .X file
sFrame *g_pParentFrame = NULL;

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);
long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam);

BOOL DoInit();
BOOL DoShutdown();
BOOL DoFrame();
BOOL SetupMesh();

sFrame *LoadFile(char *Filename);
sFrame *ParseXFile(char *Filename);
void ParseXFileData(IDirectXFileData *pDataObj, sFrame *ParentFrame);
void DrawFrame(sFrame *Frame);

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  WNDCLASSEX wcex;
  MSG        Msg;

  g_hInst = hInst;

  // Create the window class here and register it
  wcex.cbSize        = sizeof(wcex);
  wcex.style         = CS_CLASSDC;
  wcex.lpfnWndProc   = WindowProc;
  wcex.cbClsExtra    = 0;
  wcex.cbWndExtra    = 0;
  wcex.hInstance     = hInst;
  wcex.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wcex.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wcex.hbrBackground = NULL;
  wcex.lpszMenuName  = NULL;
  wcex.lpszClassName = g_szClass;
  wcex.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
  if(!RegisterClassEx(&wcex))
    return FALSE;

  // Create the Main Window
  g_hWnd = CreateWindow(g_szClass, g_szCaption,
        WS_CAPTION | WS_SYSMENU,
        0, 0, 400, 400,
        NULL, NULL,
        hInst, NULL );
  if(!g_hWnd)
    return FALSE;
  ShowWindow(g_hWnd, SW_NORMAL);
  UpdateWindow(g_hWnd);

  // Run init function and return on error
  if(DoInit() == FALSE)
    return FALSE;

  // Start message pump, waiting for signal to quit
  ZeroMemory(&Msg, sizeof(MSG));
  while(Msg.message != WM_QUIT) {
    if(PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE)) {
      TranslateMessage(&Msg);
      DispatchMessage(&Msg);
    }
    if(DoFrame() == FALSE)
      break;
  }

  // Run shutdown function
  DoShutdown();
  
  UnregisterClass(g_szClass, hInst);

  return Msg.wParam;
}

long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam)
{
  switch(uMsg) {
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

  }

  return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

BOOL DoInit()
{
  D3DPRESENT_PARAMETERS d3dpp;
  D3DDISPLAYMODE        d3ddm;
  D3DXMATRIX matProj, matView;

  // Do a windowed mode initialization of Direct3D
  if((g_pD3D = Direct3DCreate8(D3D_SDK_VERSION)) == NULL)
    return FALSE;
  if(FAILED(g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
    return FALSE;
  ZeroMemory(&d3dpp, sizeof(d3dpp));
  d3dpp.Windowed = TRUE;
  d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
  d3dpp.BackBufferFormat = d3ddm.Format;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
  if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, g_hWnd,
                                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                  &d3dpp, &g_pD3DDevice)))
    return FALSE;

  // Set the rendering states
  g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
  g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE);

  // Create and set the projection matrix
  D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4.0f, 1.33333f, 1.0f, 1000.0f);
  g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);

  // Create and set the view matrix
  D3DXMatrixLookAtLH(&matView,                                \
                     &D3DXVECTOR3(0.0f, 50.0f, -150.0f),     \
                     &D3DXVECTOR3(0.0f, 50.0f, 0.0f),          \
                     &D3DXVECTOR3(0.0f, 1.0f, 0.0f));
  g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView);

  // Load a skinned mesh from an .X file
  g_pParentFrame = LoadFile("warrior.x");

  return TRUE;
}

BOOL DoShutdown()
{
  // Release frames and meshes
  if(g_pParentFrame != NULL)
    delete g_pParentFrame;

  // Release device and 3D objects
  if(g_pD3DDevice != NULL)
    g_pD3DDevice->Release();

  if(g_pD3D != NULL)
    g_pD3D->Release();

  return TRUE;
}

BOOL DoFrame()
{
  D3DXMATRIX matWorld;

  // Clear device backbuffer
  g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
                      D3DCOLOR_RGBA(0,64,128,255), 1.0f, 0);

  // Begin scene
  if(SUCCEEDED(g_pD3DDevice->BeginScene())) {

    // Set world transformation to rotate on Y-axis
    D3DXMatrixRotationY(&matWorld, (float)timeGetTime() / 1000.0f);
    g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);

    // Draw frames
    DrawFrame(g_pParentFrame);

    // Release texture
    g_pD3DDevice->SetTexture(0, NULL);

    // End the scene
    g_pD3DDevice->EndScene();
  }

  // Display the scene
  g_pD3DDevice->Present(NULL, NULL, NULL, NULL);

  return TRUE;
}


sFrame *LoadFile(char *Filename)
{
  return ParseXFile(Filename);
}

sFrame *ParseXFile(char *Filename)
{
  IDirectXFile           *pDXFile = NULL;
  IDirectXFileEnumObject *pDXEnum = NULL;
  IDirectXFileData       *pDXData = NULL;
  sFrame                 *Frame;

  // Create the file object
  if(FAILED(DirectXFileCreate(&pDXFile)))
    return FALSE;

  // Register the templates
  if(FAILED(pDXFile->RegisterTemplates((LPVOID)D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES))) {
    pDXFile->Release();
    return FALSE;
  }

  // Create an enumeration object
  if(FAILED(pDXFile->CreateEnumObject((LPVOID)Filename, DXFILELOAD_FROMFILE, &pDXEnum))) {
    pDXFile->Release();
    return FALSE;
  }

  // Allocate a frame that becomes root
  Frame = new sFrame();

  // Loop through all objects looking for the frames and meshes
  while(SUCCEEDED(pDXEnum->GetNextDataObject(&pDXData))) {
    ParseXFileData(pDXData, Frame);
    ReleaseCOM(pDXData);
  }

  // Release used COM objects
  ReleaseCOM(pDXEnum);
  ReleaseCOM(pDXFile);

  // Return root frame
  return Frame;
}

void ParseXFileData(IDirectXFileData *pDataObj, sFrame *ParentFrame)
{
  IDirectXFileObject *pSubObj  = NULL;
  IDirectXFileData   *pSubData = NULL;
  IDirectXFileDataReference *pDataRef = NULL;
  const GUID *Type = NULL;
  char       *Name = NULL;
  DWORD       Size;

  sFrame     *Frame = NULL;
  sFrame     *SubFrame = NULL;

  sMesh         *Mesh = NULL;
  ID3DXBuffer   *MaterialBuffer = NULL;
  D3DXMATERIAL  *Materials = NULL;
  ID3DXBuffer   *Adjacency = NULL;
  DWORD         *AdjacencyIn = NULL;
  DWORD         *AdjacencyOut = NULL;

  DWORD i;

  // Get the template type
  if(FAILED(pDataObj->GetType(&Type)))
    return;

  // Get the template name (if any)
  if(FAILED(pDataObj->GetName(NULL, &Size)))
    return;
  if(Size) {
    if((Name = new char[Size]) != NULL)
      pDataObj->GetName(Name, &Size);
  }

  // Give template a default name if none found
  if(Name == NULL) {
    if((Name = new char[9]) == NULL)
      return;
    strcpy(Name, "Template");
  }

  // Set sub frame
  SubFrame = ParentFrame;

  // Process the templates

  // Frame
  if(*Type == TID_D3DRMFrame) {
    // Create a new frame structure
    Frame = new sFrame();

    // Store the name
    Frame->m_Name = Name;
    Name = NULL;

    // Add to parent frame
    Frame->m_Sibling = ParentFrame->m_Child;
    ParentFrame->m_Child = Frame;

    // Set sub frame parent
    SubFrame = Frame;
  }

  // Load a mesh
  if(*Type == TID_D3DRMMesh) {
    // Create a new mesh structure
    Mesh = new sMesh();

    // Store the name
    Mesh->m_Name = Name;
    Name = NULL;

    // Load mesh data (as a skinned mesh)
    if(FAILED(D3DXLoadSkinMeshFromXof(pDataObj, 0,
                g_pD3DDevice,
                &Adjacency,
                &MaterialBuffer, &Mesh->m_NumMaterials, 
                &Mesh->m_BoneNames, &Mesh->m_BoneTransforms,
                &Mesh->m_SkinMesh))) {
      delete Mesh;
      return;
    }

    // Convert to regular mesh if no bones
    if(!(Mesh->m_NumBones = Mesh->m_SkinMesh->GetNumBones())) {
    
      // Convert to a regular mesh
      Mesh->m_SkinMesh->GetOriginalMesh(&Mesh->m_Mesh);
      ReleaseCOM(Mesh->m_SkinMesh);

    } else {

      // Get a pointer to bone matrices
      Mesh->m_BoneMatrices = (D3DXMATRIX*)Mesh->m_BoneTransforms->GetBufferPointer();

      // Allocate buffers for adjacency info
      AdjacencyIn  = (DWORD*)Adjacency->GetBufferPointer();
      AdjacencyOut = new DWORD[Mesh->m_SkinMesh->GetNumFaces() * 3];

      // Generate the skin mesh object
      if(FAILED(Mesh->m_SkinMesh->GenerateSkinnedMesh(
                  D3DXMESH_WRITEONLY, 0.0f, 
                  AdjacencyIn, AdjacencyOut, NULL, NULL, 
                  &Mesh->m_Mesh))) {
        // Convert to a regular mesh if error
        Mesh->m_SkinMesh->GetOriginalMesh(&Mesh->m_Mesh);
        ReleaseCOM(Mesh->m_SkinMesh);
        Mesh->m_NumBones = 0;
      }

      delete [] AdjacencyOut;
      ReleaseCOM(Adjacency);
    }

    // Load materials or create a default one if none
    if(!Mesh->m_NumMaterials) {
      // Create a default one
      Mesh->m_Materials = new D3DMATERIAL8[1];
      Mesh->m_Textures  = new LPDIRECT3DTEXTURE8[1];

      ZeroMemory(Mesh->m_Materials, sizeof(D3DMATERIAL8));
      Mesh->m_Materials[0].Diffuse.r = 1.0f;
      Mesh->m_Materials[0].Diffuse.g = 1.0f;
      Mesh->m_Materials[0].Diffuse.b = 1.0f;
      Mesh->m_Materials[0].Diffuse.a = 1.0f;
      Mesh->m_Materials[0].Ambient   = Mesh->m_Materials[0].Diffuse;
      Mesh->m_Materials[0].Specular  = Mesh->m_Materials[0].Diffuse;
      Mesh->m_Textures[0] = NULL;

      Mesh->m_NumMaterials = 1;
    } else {
      // Load the materials
      Materials = (D3DXMATERIAL*)MaterialBuffer->GetBufferPointer();
      Mesh->m_Materials = new D3DMATERIAL8[Mesh->m_NumMaterials];
      Mesh->m_Textures  = new LPDIRECT3DTEXTURE8[Mesh->m_NumMaterials];

      for(i=0;i<Mesh->m_NumMaterials;i++) {
        Mesh->m_Materials[i] = Materials[i].MatD3D;
        Mesh->m_Materials[i].Ambient = Mesh->m_Materials[i].Diffuse;
     
        // Build a texture path and load it
        if(FAILED(D3DXCreateTextureFromFile(g_pD3DDevice,
                                            Materials[i].pTextureFilename,
                                            &Mesh->m_Textures[i]))) {
          Mesh->m_Textures[i] = NULL;
        }
      }
    }
    ReleaseCOM(MaterialBuffer);

    // Link in mesh
    Mesh->m_Next = ParentFrame->m_Mesh;
    ParentFrame->m_Mesh = Mesh;
  }

  // Skip animation sets and animations
  if(*Type == TID_D3DRMAnimationSet || *Type == TID_D3DRMAnimation || *Type == TID_D3DRMAnimationKey) {
    delete [] Name;
    return;
  }

  // Release name buffer
  delete [] Name;

  // Scan for embedded templates
  while(SUCCEEDED(pDataObj->GetNextObject(&pSubObj))) {

    // Process embedded references
    if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileDataReference, (void**)&pDataRef))) {
      if(SUCCEEDED(pDataRef->Resolve(&pSubData))) {
        ParseXFileData(pSubData, SubFrame);
        ReleaseCOM(pSubData);
      }
      ReleaseCOM(pDataRef);
    }

    // Process non-referenced embedded templates
    if(SUCCEEDED(pSubObj->QueryInterface(IID_IDirectXFileData, (void**)&pSubData))) {
      ParseXFileData(pSubData, SubFrame);
      ReleaseCOM(pSubData);
    }
    ReleaseCOM(pSubObj);
  }

  return;
}

void DrawFrame(sFrame *Frame)
{
  sMesh *Mesh;
  D3DXMATRIX *Matrices;
  DWORD i;

  // Return if no frame
  if(Frame == NULL)
    return;
  
  // Draw meshes if any in frame
  if((Mesh = Frame->m_Mesh) != NULL) {

    // Generate mesh from skinned mesh to draw with
    if(Mesh->m_SkinMesh != NULL) {
      // Allocate an array of matrices to orient bones
      Matrices = new D3DXMATRIX[Mesh->m_NumBones];

      // Set all bones orientation to identity
      for(i=0;i<Mesh->m_NumBones;i++)
        D3DXMatrixIdentity(&Matrices[i]);

      // Update skinned mesh
      Mesh->m_SkinMesh->UpdateSkinnedMesh(Matrices, NULL, Mesh->m_Mesh);

      // Render the mesh
      for(i=0;i<Mesh->m_NumMaterials;i++) {
        g_pD3DDevice->SetMaterial(&Mesh->m_Materials[i]);
        g_pD3DDevice->SetTexture(0, Mesh->m_Textures[i]);
        Mesh->m_Mesh->DrawSubset(i);
      }

      // Free array of matrices
      delete [] Matrices;
    }

    // Go to next mesh
    Mesh = Mesh->m_Next;
  }

  // Draw child frames
  DrawFrame(Frame->m_Child);

  // Draw sibling frames
  DrawFrame(Frame->m_Sibling);
}

  • Сохрани Решение (Файл -> Сохранить все).
Данный исходный код целиком взят из примера к книге Programming Role-Playing Games with DirectX by Jim Adams (01 Jan 2002). Автор прогал на MS Visual С++ 6.0 (вышла в свет в 1998 г.) с установленным DirecX SDK 8.0. И если в нашем случае в качестве Graphic API мы юзаем практически идентичный DirectX SDK 8.1, то IDE у нас совсем другая - MS Visual С++ 2010 Express Edition. Нет, не с целью усложнения задачи. Просто MS Visual С++ 6.0 в своё время стоила 500 USD. Сейчас её, в принципе, можно поискать в торрентах. (За всё, что ты там скачаешь, администрация igrocoder.ru ответственности не несёт!) Но использование платного/пиратского софта не соответствует концепции сайта igrocoder.ru . Поэтому наш выбор:
  • Бесплатная IDE MS Visual С++ 2010 Express edition;
  • Бесплатный DirectX SDK 8.1 .
К концу данной статьи мы вновь докажем, что игрокодинг (под ОС Windows) в домашних условиях возможен. И для него нужны только компьютер с ОС Windows и доступ к Интернету.
Но вернёмся к нашему Проекту XFile01. С момента выхода MS Visual С++ 6.0 прошло немало времени. С MSVC++2010 Express их разделяют аж 12 лет. Из-за этого вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться не будет, выдавая многочисленные ошибки. Даже с настроенными путями к DirectX SDK. Но мы это исправим.

Готовим Проект XFile01 к компиляции

Для успешной компиляции изменим настройки (=свойства) текущего Проекта XFile01, созданного в MSVC++2010. При этом сам код из книги 2002 года останется нетронутым. Или почти нетронутым. В ходе тестирования выяснилось, что косячный автор в своём коде вызывает функцию GenerateSkinnedMesh с пятью параметрами, вместо положенных семи. Вот её прототип из документации DirectX SDK 8:
Прототип функции GenerateSkinnedMesh (DirectX 8)
HRESULT GenerateSkinnedMesh(
	DWORD Options,
	FLOAT minWeight,
	CONST LPDWORD pAdjacencyIn,
	LPDWORD pAdjacencyOut,
	DWORD* pFaceRemap,
	LPD3DXBUFFER* ppVertexRemap,
	LPD3DXMESH* ppMesh
	);

Также функцию UpdateSkinnedMesh он вызывает с двумя параметрами, вместо положенных трёх. Вот её прототип из документации DirectX SDK 8:
Прототип функции UpdateSkinnedMesh (DirectX 8)
HRESULT UpdateSkinnedMesh(
	CONST D3DXMATRIX* pBoneTransforms,
	CONST D3DXMATRIX* pBonelnvTransforms,
	LPD3DXMESH pMesh
	);

За числом параметров в своих функциях DirectX следит строго, в противном случае выдавая многочисленные ошибки в ходе компиляции. В приведённом выше исходном коде мы доставили на месте недостающих параметров значения NULL и всё пошло как по маслу.
Закрыть
noteОбрати внимание

Напомним, что такую настройку необходимо повторно проделывать при создании каждого нового Проекта. Ниже представлен алгоритм действий по настройке Проекта, созданного в MSVC++2010 Express с применением DirectX SDK. При создании приложений под платформы, отличные от Win32, либо применении более новых версий DirectX SDK, процесс конфигурирования Проекта может отличаться от приведённого ниже.


Указываем пути к DirectX SDK 8.1

Если попытаться скомпилировать Проект XFile01 в таком виде, то ничего не выйдет.
В единственном файле исходного кода WinMain.cpp можно увидеть инклуды различных заголовочных файлов DirectX (весии 8):
Фрагмент WinMain.cpp
...
// Include files
#include <windows.h>
#include <stdio.h>
#include "d3d8.h"
#include "d3dx8.h"
#include "dxfile.h"
#include "rmxfguid.h"
#include "rmxftmpl.h"
...

На данном этапе MSVC++2010 ничего не знает об их местоположении. В статье MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK мы указывали пути к DirectX SDK (версии 9 и выше). Для DirectX SDK 8 это делается аналогично. Начнём.
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • Убедись, что DirectX SDK 8.1 установлен на компьютере и ты уверенно можешь назвать полный путь к его каталогу.
  • В Обозревателе решений видим: "Решение "XFile01"", а строкой ниже жирным шрифтом название Проекта (тоже XFile01).
  • Жмём правой кнопкой мыши по названию Проекта XFile01. Во всплывающем меню выбираем пункт "Свойства".
Image
Или в Главном меню выбираем Проект->Свойства. Или нажимаем Alt+F7.
  • В появившемся меню свойств проекта выбираем Свойства конфигурации -> Каталоги VC++.
В правой части этой страницы расположены пути ко всевозможным каталогам. Здесь нас интересуют только 2 строки: Каталоги включения и Каталоги библиотек.

Указываем каталог включений (include) DirectX SDK 8.1
  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги включения. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
Image
  • В появившемся меню "Каталоги включения" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к заголовочным файлам DirectX SDK 8.1 (include).
В нашем случае это C:\DXSDK8\include. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
Image
  • Жмём "OK".

Указываем каталог библиотек (lib) DirectX SDK 8.1
  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги библиотек. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
  • В появившемся меню "Каталоги библиотек" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к 32-разрядным версиям файлов библиотек DirectX SDK 8.1 (lib). В нашем случае это c:\DXSDK8\lib\. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
Image
  • Жмём "ОК".
  • На Странице свойств тоже жмём "ОК".
  • Сохрани Решение (File -> Save All).
Готово.

Указываем пути к Windows SDK

Помимо указания путей к заголовкам (include) и библиотекам (lib) DirectX SDK, для любого DirectX-Проекта также необходимо указать пути к заголовкам (include) и библиотекам (lib) Windows SDK. Самое смешное, что в MS Visual C++ 2010 эти пути указываются по умолчанию для каждого создаваемого Проекта. В этом нетрудно убедиться, если ещё раз открыть Проект -> Свойства -> Свойства конфигурации - >Каталоги VC++ -> Каталоги включения -> Изменить. В окне "Каталоги включения" в нижней (недоступной для редактирования) части видим список "Унаследованные значения", где в третьей строке стоит значение:
$(WindowsSdkDir)include

В результате видим, что добавленные пути к DirectX SDK (в обоих окнах: include и lib) расположены вверху списка, а пути к Windows SDK - в недоступной области, на несколько строк ниже.
Но! Заголовочным файлам DirectX SDK "жизненно важно", чтобы они включались после включений заголовков Windows SDK.
Закрыть
noteВажно!

Каталоги, пути к которым указаны в окнах "Каталоги включения" и "Каталоги библиотек" при компиляции считываются один за другим по списку сверху вниз. Поэтому для корректного указания путей надо разместить пути к Windows SDK выше, а к DirectX SDK - ниже по списку (чтобы они считывались последними).

В данной ситуации мы не можем поднять пути к Windows SDK, прописанные по умолчанию при создании Проекта, т.к. они расположены в специальной нередактируемой области (ограничение бесплатной версии MSVC++2010 Express). Но можем схитрить и добавить ещё раз пути к тем же самым каталогам Windows SDK, подняв эти строки выше строк DirectX SDK.
Выше мы указывали пути к DirectX SDK. Для путей к Windows SDK это делается аналогично. Начнём.
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект, с которым работаешь в данный момент.
  • Убедись, что Windows SDK (в нашем случае версия 7) установлен на компьютере и ты уверенно можешь назвать полный путь к его каталогу.
В Обозревателе решений видим: "Решение "..."", а строкой ниже жирным шрифтом название Проекта с тем же названием (если не менял вручную).
  • Жмём правой кнопкой мыши по названию Проекта. Во всплывающем меню выбираем пункт "Свойства".
Image
Или в Главном меню выбираем Проект->Свойства. Или нажимаем Alt+F7.
В появившемся меню свойств проекта выбираем Свойства конфигурации -> Каталоги VC++ . В правой части этой страницы расположены пути ко всевозможным каталогам. Здесь нас, как и в прошлый раз, интересуют только 2 строки: Каталоги включения и Каталоги библиотек.

Указываем каталог включений (include) Windows SDK
  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги включения. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
Image
В появившемся меню "Каталоги включения" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к заголовочным (include) файлам Windows SDK. В нашем случае (Win7 x64) это C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
Image
  • Меняй порядок считывания каталогов включений с помощью кнопок с чёрными стрелками в верхней части окна "Каталоги вложений".
Каталог включений DirectX SDK должен всегда стоять в самом конце списка, как на этом скриншоте:
Image
  • Жмём "ОК".

Указываем каталог библиотек (lib) Windows SDK
  • В меню свойств Проекта щёлкаем левой кнопкой мыши по пункту Каталоги библиотек. В правой части этой строки видим кнопку с чёрным треугольником, указывающим на наличие выпадающего меню. Нажимаем на неё -> выбираем "Изменить..."
  • В появившемся меню "Каталоги библиотек" жмём кнопку "Создать строку" (с жёлтой папкой) и указываем полный путь к папке с файлами библиотек (lib) Windows SDK. В нашем случае (Win7 x64) это C:\Program Files (x86)\MicrosoftSDKs\Windows\v7.0A\Lib. Можно просто выбрать каталог из дерева каталогов, нажав кнопку с троеточием, расположенную справа от строки ввода.
Image
  • Меняй порядок считывания каталогов вложений с помощью кнопок с чёрными стрелками в верхней части окна.
Каталог библиотек DirectX SDK должен всегда стоять в самом конце списка, как на этом скриншоте:
Image
  • Жмём "ОК".
  • На Странице свойств тоже жмём "ОК".
  • Сохрани Решение (File -> Save All). Готово.

Выбираем многобайтовую кодировку

Закрыть
noteПримечание

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде.
Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, данный Проект мы настроим под многобайтовую кодировку.

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию только что созданного Проекта XFile01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".
Image
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все).

Отключаем инкрементную компоновку (incremental linking)

Инкрементная компоновка призвана сократить время компилирования. Но на деле её присутствие часто вызывает ошибки вроде этой:
Error LNK1123: сбой при преобразовании в COFF: файл недопустим или поврежден


Закрыть
warningОшибка LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt

В MS Visual C++ 2010 даже компиляция консольных приложений нередко завершается неудачей, а вместо исполняемого файла программист видит сообщение:
LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt.

ПРИЧИНА: MS Visual С++ 2010 "не нравится" версия .NET Framework(external link), установленная в операционной системе. В общих чертах, MS Visual С++ 2010 спрограммирована для работы под управлением .NET Framework 4.0 и сильно к нему привязана. Если точнее, к нему сильно привязана система инкрементной компоновки приложений (incremental linking), которая по умолчанию включена для всех создаваемых проектов.
Во время установки MS Visual C++ 2010 пытается установить свой "родной" .NET Framework 4.0, проверяя версию этой программной платформы, установленную в ОС на данный момент. Если версия .NET Framework ниже 4.0, то она обновляется до 4.0 и всё прекрасно компилируется. Если версия .NET Framework выше 4.0, то всё оставляется как есть: IDE успешно завершает установку, но при компиляции ВСЕХ приложений выскакивает данная ошибка. Более того, ошибка была замечена даже при наличии в системе .NET Framework версии 4.0, но отличающейся от "родной" припиской вроде "Beta" или "Release Candidate".

ВАРИАНТЫ РЕШЕНИЙ:
1. Отключить инкрементную линковку в опциях Проекта.
В главном меню MSVC++2010 выбираем: Проект->Свойства->Свойства конфигурации->Компоновщик(Linker)->Включить инкрементное построение (Incremental Linking). Данный пункт по умолчанию включен для всех новых проектов. Выставляем его в Нет (No). И жмём OK. Инкрементное построение заметно сокращает время компилирования больших проектов. Но в нашем случае его отсутствие некритично.
2. Удалить (переместить в другое место) утилиту cvtres.exe из каталога bin установленной MS Visual C++ 2010.
В нашем случае (Win7 x64) полный путь до данного файла такой: C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cvtres.exe . Официальное название данного приложения - Microsoft® Resource File To COFF Object Conversion Utility (утилита конвертации файлов двоичных ресурсов в Component Object File Format). Опытным путём установлено, что при линковке IDE "спотыкается" именно об него.
3. Удалить из системы все версии .NET Framework (включая языковые пакеты и всякие профайлеры, если есть) и саму MS Visual C++ 2010.
Всё вышеперечисленное можно без труда найти в меню "Программы и компоненты" (MS Windows Vista/7/8). Затем заново установить MS Visual C++ 2010. При этом автоматом установится .NET Framework 4.0, идущий с ней в наборе.

Третий пункт - самый долгий. На деле почти всегда хватает выполнения первых двух. Данным вопросом озадачивались ребята здесь: https://www.cyberforum.ru/cpp-beginners/thread637174.html?ysclid=l3akpmanrq(external link). После этого линковка (=компоновка) проходит идеально. Подобные "костыли" в IDE от Майков - не редкость. Можно предположить, что команда с головой ударилась в тестирование .NET-возможностей MSVC++2010, совсем забыв о Win32-направлении (либо признав его бесперспективным).


Отключим инкрементную компоновку в свойствах открытого Проекта. Для MS Visual C++ 2010 порядок следующий:
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект, с которым работаешь.
В Обозревателе решений видим: "Решение "..."", а строкой ниже жирным шрифтом название Проекта. Обычно с тем же названием (если не менял вручную).
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации -> Компоновщик -> Общие (Configuration Properties -> Linker -> General), в правой части в строке "Включить инкрементную компоновку" ставим значение Нет (/INCREMENTAL:NO).
  • Жмём ОК.

Прописываем библиотеки d3dx8.lib, d3d8.lib, dxguid.lib, d3dxof.lib и winmm.lib в окне "Дополнительные зависимости" (Additional dependencies) компоновщика (Linker)

Данный этап проходил даже автор кода Jim Adams в 2002 году, прогая на MSVC++6.0. В начальных комментариях листинга WinMain.cpp он намекнул, что библиотеки d3dx8.lib, d3d8.lib, winmm.lib и dxguid.lib необходимо явно указывать в списке дополнительных зависимостей линкера (=компоновщика):
Фрагмент WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 6 XFile/Skinned Mesh Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)

Required libraries:
  WINMM.LIB, D3D8.LIB, D3DX8.LIB, and DXGUID.LIB
**************************************************/
...

Библиотека WINMM.LIB расположена в папке с установленным Windows SDK (в нашем случае по пути C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib), пути к которой мы уже прописали выше.
Библиотеки D3D8.LIB, D3DX8.LIB и DXGUID.LIB расположены в папке с установленным DirectX SDK 8 (в нашем случае по пути C:\DXSDK8\lib), пути к которой мы также прописали выше.
На деле выяснилось, что одна из функций примера также юзает библиотеку d3dxof.lib. Поэтому её тоже добавим. Она также расположена в папке с установленным DirectX SDK 8.
Т.к. мы создаём исполняемое приложение (исполняемый .ехе-файл), а не библиотеку, то после компиляции полученный объектный модуль сразу линкуется путём вызова компоновщика (linker). Так вот, этот самый компоновщик по ранее прописанным каталогам данные библиотеки не ищет. Поэтому их необходимо указывать отдельно в окне настроек Проекта, в разделе "Компоновщик".
ОК, начинаем.
  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект XFile01.
  • В Главном меню MS Visual С++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта последовательно щёлкаем по раскрывающимся ветвям иерархического дерева: Свойства конфигурации -> Компоновщик -> Ввод (Configuration Properties -> Linker -> Input).
  • В правой части, напротив строки "Дополнительные зависимости" жмём кнопку с чёрным треугольником.
  • Во всплывающем списке жмём "Изменить".
  • В появившемся окне "Дополнительные зависимости" в верхнем поле ввода прописываем в столбик (один под другим) имена файлов пяти библиотек:
d3dx8.lib
d3d8.lib
dxguid.lib
d3dxof.lib
Winmm.lib
Image
  • Жмём ОК, ОК.
  • Сохрани Решение (Файл->Сохранить все).

Отключаем использование компоновщиком библиотеки libci.dll

Да, даже на данном этапе компиляция Проекта выдаст ошибку. Библиотека libci.dll использовалась в VisualStudio когда-то очень давно, и в современных версях IDE её нет. Тем не менее компоновщик почти всегда вызывает её при компиляции, ругаясь на её отсутствие. Самый простой способ это исправить - запретить использовать libci.dll по умолчанию.
ОК, начнём.
  • Убедись, что MSVC++2010 запущена и в ней открыт Проект, с которым работаешь в данный момент.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта последовательно щёлкаем по раскрывающимся ветвям иерархического дерева: Свойства конфигурации -> Компоновщик -> Командная строка (Configuration Properties -> Linker -> Command Promt).
В правой части, внизу, видим поле ввода "Дополнительные параметры".
  • Пишем в него строку: /NODEFAULTLIB:libci
Image
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все).

Компилируем Проект XFile01

Наконец, наш тестовый Проект готов к компиляции.
  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 нак лавиатуре.
Image
После компилирования приложение XFile01 автоматически запустится и покажет окно с голубым фоном. Это нормально, т.к. .Х-файл и его текстуру пока никто не готовил.
Закрыть
noteОбрати внимание

Если попытаться запустить скомпилированный .exe-файл на компьютере без установленной MSVC++2010, то экзешник (скорее всего) "ругнётся" на отсутствующую библиотеку MSVCR100D.dll. Буква D в её имени означает Debug, т.е. данное приложение скомпилировано с отладочной конфигурацией (профилем), которая выставляется по умолчанию у каждого вновь создаваемого Проекта/Решения. При релизе игры приложение напротив, компилируют с конфигурацией Release (релиз). При этом при создании инсталлятора в дистрибутив с игрой добавляют т.н. набор библиотек времени выполнения (в нашем слчае MS Visual C++ 2010 Runtime Redistributable). Он устанавливается вместе с игрой, т.к. без него игра тупо не стартанёт. Если для Release-профиля такой набор нетрудно нагуглить (например здесь: https://www.mydigitallife.net/visual-c-2010-runtime-redistributable-package-x86-x64-ia64-free-download(external link), 4,8 Мб для 64-разрядной версии ОС), то для запуска Debug-версии на отдельном компе на него потребуется установить целую MSVC++2010. Всё так сложно в том числе с целью не допустить утечек предрелизных разработок с компов игрокодерских компаний. Релиз есть релиз. А с дебаг-версиями, как правило, работают только сами игрокодеры, на своих компах отлавливая ошибки.
Конфигурации Debug и Release легко переключаются на странице свойств открытого Проекта (в MSVC++2010 в главном меню выбираем Проект->Свойства, в верхней части диалога видим всплывающий список "Конфигурация").


Готовим .X файл

В конце реализации авторской функции Dolnit видим функцию загрузки .Х-файла warror.x:
Фрагмент WinMain.cpp
...
  // Load a skinned mesh from an .X file
  g_pParentFrame = LoadFile("warrior.x");
...

Очевидно, что здесь можно указать любое другое имя файла.
  • Создай в любом 3D-редакторе или скачай с Интернета любой файл модели с расширением .х .
Приложение XFile01 откроет далеко не каждый .Х-файл, даже если тот переименовать в warrior.x. Например, файл tiger.x, расположенный в папке с примерами DirectX SDK 8 (в нашем случае c:\DXSDK8\samples\Multimedia\Direct3D\Tutorials\Tut06_Meshes\) не загружается и при запуске XFile01.exe мы видим всё то же окно с голубым фоном.
В целях экономии времени возьми файл warrior.x из архива с примером от Jim Adams: https://disk.yandex.ru/d/pyoDHEdm1j514g(external link). К сожалению, автор книги не рассказал, с какими сеттингами он создавал данный .Х-файл.
  • Помести его в папку с исполняемым файлом Проекта XFile01.
В нашем случае путь до него такой: С:\users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\XFile01\Debug .
  • Повторно запусти XFile01.exe.
Видим в окне вращающуюся модель без текстуры (модель окрашена в белый цвет).

Готовим текстуру

По умолчанию DirectX-приложения ищут сопутствующую текстуру в том же каталоге, где расположен .Х-файл. Причём их имена должны совпадать.
  • Создай в любом графическом редакторе (проще всего в обычном Paint) файл растрового изображения.
  • Сконвертируй его в формат .bmp и переименуй в warrior.bmp.
Как вариант, файл изображения можно скачать из Интернета. Желательно чтобы файл имел небольшое разрешение (например 256x256) и 256-цветную палитру.
  • Помести его в папку с исполняемым файлом Проекта XFile01.
В нашем случае путь до него такой: С:\users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\XFile01\Debug
  • Повторно запусти XFile01.exe.
Видим в окне вращающуюся модель с нанесённой текстурой.

Ссылка на готовый Проект XFile01

https://disk.yandex.ru/d/pyoDHEdm1j514g(external link) (ZIP-архив; 67 Кб). В архив также включены .Х-файл с мешем и .BMP-файл текстуры.

Задание на дом

В папке с примерами (samples) DirectX SDK 8 есть пример "Tut06_Meshes", также демонстрирующий загрузку меша из .X-файла. В нашем случае путь до него такой: c:\DXSDK8\samples\Multimedia\Direct3D\Tutorials\Tut06_Meshes\ .
  • Найди в папке с данным примером файл исходного кода Meshes.срр.
  • Создай новый Проект Meshes01 в MSVC++2010 и скомпилируй исходный код, содержащийся в Meshes.срр.
Смотри выше инструкции по подготовке Проекта к компиляции в MSVC++2010 Express Edition.
  • Зрительно сравни исходные коды примеров Джима Адамса и MS DirectX SDK 8, выяви сходства и различия.
  • Подумай, почему модель тигра из данного примера (tiger.x), "подсунутая" приложению XFile01.exe под именем warrior.x не хочет открываться.
  • Проведи обратный эксперимент, "подсунув" скачанные модели приложению, полученному в результате компиляции Проекта Meshes01.
В папке с установленным DirectX SDK 8 есть чудная программа mview.exe (MeshViewer), загружающая .Х-файлы и показывающая меш в окне. В нашем случае путь до неё такой: c:\DXSDK8\bin\DXUtils\ .
  • Запусти программу mview.exe.
  • Открой в ней файл warrior.х из примера к данному уроку. (В главном меню жми File->Open Mesh File...)
  • Открой окно иерархии фреймов. (В главном меню жми View->Hierarchy.)
В появившемся окне видим свёрнутое дерево фреймов с ключевым фреймом Unnamed Frame. Слева от его имени видим символ + .
  • Щёлкни левой кнопкой мыши по символу + .
Там будет ещё один дочерний фрейм Bip01 со своим символом + .
  • Последовательно нажимая на символы +, раскрой все ветви иерархии фреймов. Изучи их "говорящие" имена и расположение в иерархии.
  • Проделай те же операции с файлом tiger.х, расположенном в папке с примерами установленного DiretcX SDK 8.
В нашем случае путь до него такой: c:\DXSDK8\samples\Multimedia\Direct3D\Tutorials\Tut06_Meshes\ .
Вероятно, решающее значение для корректного открытия .X файла имеет метод его сохранения из 3D-редактора. Один из примеров корректных сеттингов в окне импорта 3DS Мах представлен здесь: http://www.cgdev.net/axe/download.php(external link) .

Исследуем код примера

В начале листинга видим пару структур:
  • sMesh - описывает загружаемый меш.
  • sFrame - содержит иерархию фреймов + матрицы трансформаций каждого фрейма.

Загрузка скинированного меша

Функция-обёртка (wrapper function) LoadMesh вызывает функцию ParseXFile:
Фрагмент WinMain.cpp
...
sFrame *LoadFile(char *Filename)
{
  return ParseXFile(Filename);
}

sFrame *ParseXFile(char *Filename)
{
  IDirectXFile           *pDXFile = NULL;
  IDirectXFileEnumObject *pDXEnum = NULL;
  IDirectXFileData       *pDXData = NULL;
  sFrame                 *Frame;

  // Create the file object
  if(FAILED(DirectXFileCreate(&pDXFile)))
    return FALSE;

  // Register the templates
  if(FAILED(pDXFile->RegisterTemplates((LPVOID)D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES))) {
    pDXFile->Release();
    return FALSE;
  }

  // Create an enumeration object
  if(FAILED(pDXFile->CreateEnumObject((LPVOID)Filename, DXFILELOAD_FROMFILE, &pDXEnum))) {
    pDXFile->Release();
    return FALSE;
  }

  // Allocate a frame that becomes root
  Frame = new sFrame();

  // Loop through all objects looking for the frames and meshes
  while(SUCCEEDED(pDXEnum->GetNextDataObject(&pDXData))) {
    ParseXFileData(pDXData, Frame);
    ReleaseCOM(pDXData);
  }

  // Release used COM objects
  ReleaseCOM(pDXEnum);
  ReleaseCOM(pDXFile);

  // Return root frame
  return Frame;
}
...

Обе функции авторские. Шаблоны фреймов (Frame Tamplates), по мере считывания (=энумерации) функцией LoadFile, добавляются в общую иерархию фреймов. Все остальные шаблоны (не являющиеся шаблонами фреймов) также энумерируются в виде серии дочерних (точнее, не имеющих своих потомков) шаблонов.
При обнаружении шаблона меша (Mesh Template) во время энумерирования .Х-файла функция LoadFile загружает меш путём вызова DirectX-функции D3DXLoadSkinMeshFromXof. Сразу после этого объект меша добавляется в связный список (linked list) загруженных мешей. У каждого фрейма есть указатель на тот или иной загруженный меш. Это позволяет на основе одного меша создавать несколько фреймов.
Как только меш окажется загруженным, функция загрузки автоматически сопоставит кости меша с их соответствующими фреймами и затем загрузит материалы. Если меш не содержит костей, он автоматически конвертируется в стандартный меш (standart mesh).
Таким образом, для загрузки скинированного меша (или серии таких мешей) вызывают функцию LoadFile, указав в единственном параметре имя загружаемого .Х-файла:
Фрагмент WinMain.cpp
...
  // Load a skinned mesh from an .X file
  g_pParentFrame = LoadFile("warrior.x");
...

В свою очередь функция LoadFile вызывает функцию ParseXFile, которая возвращает в первую функцию указатель на фрейм, являющийся ключевым (корневым; root frame).
Сразу после загрузки меша обновляем (Update) его и рендерим (Render). Обновление меша запрашивает указатель на объект стандартного меша (standart mesh object), который затем также используется для рендеринга (меша).

Обновление и рендеринг скинированного меша (Updating and Rendering a Skinned Mesh)

Прежде чем рендерить скинированный меш, его надо сперва обновить (update). Ведь он, на самом деле, не знает, как именно отрисовывать самого себя. Для этого применяют все (родительскую + собственную) матрицы трансформации к каждой из костей и вызывают функцию ID3DXSkinMesh::UpdateSkinnedMesh, передавая ей в качестве аргумента указатель на инстанс объекта ID3DXMesh, представляющий собой итоговую форму меша с применением всех нужных деформаций.
Для изменения трансформации фрейма мы модифицируем её матрицу вращения (frame's rotation transformation matrix). Данной операции часто оказывается достаточно, т.к. дочерние фреймы не могут быть оттранслированы. Может только ключевой фрейм.
Сперва определяем, какой из фремов будем модифицировать. Для этого сканируем список фреймов и затем путём вызова функции D3DXMatrix* изменяем вращение каждого из них.
Как только вращение фреймов окончено и у каждого установлена нужная ориентация в пространстве, всё готово к обновлению всех трансформаций в иерархии фреймов. Начинают с ключевого фрейма, который сориентирован в мире именно там, где будет рендериться меш. Затем матрица трансформации ключевого фрейма передаётся следующему дочернему фрейму и комбинируется с его собственной. Этот процесс продолжается до тех пор, пока матрицы трансформаций всех фреймов не будут обновлены.

3D-анимации меша, хранимые в .Х-файлах

3D-анимация:
  • Может храниться в самом .Х-файле.
Причём в одном .Х-файле может храниться несколько анимаций.
  • Сильно отличается от своей 2D-сестрёнки.
Здесь не прокатит ручная отрисовка каждого кадра с созданием последующей анимации путём быстрой смены кадров. В мире 3D объект может быть виден с любого ракурса.
В основе 3D-анимации лежит изменение матриц трансформации фреймов, используемых для переориентирования мешей с течением времени. Таким образом меши, расположенные во фреймах могут вращаться и изменять своё положение прямо у игрока на глазах. Такое перемещение мешей и является анимацией. Меши в реальном времени могут быть:
  • перемещены (при изменении матрицы трансляции; translation matrix);
  • повёрнуты (при изменении матрицы вращения; rotation matrix);
  • увеличены или уменьшены в размерах (при изменении матрицы масштабирования; scale matrix).
При работе со скинированными мешами применение трансформации фреймов - единственный способ анимировать их (меши). Так как скинированный меш является одним (single) сплошным мешем (т.е. не составленным из нескольких мешей), для деформации вершин необходимо изменять фреймы. Самый простой способ изменения матриц трансформации фрейма - техника ключевых кадров (Key Frame Techniques).
Каждая анимация в .Х-файле представлена в виде набора специальных шаблонов анимации (animation templates). Загрузка данных из них происходит примерно тем же образом, что и загрузка скинированного меша. Загружать анимацию из .Х-файла весьма непросто. У каждой анимации надо обрабатывать шаблон анимации, шаблон набора анимации (animation set template), временные шаблоны (time templates), шаблоны ключевых кадров (key-frame templates) и т.д.
Шаблоны анимации содержат т.н. ключи (keys), которые применяются в анимации по ключевым кадрам (key frame ainamtion). Каждый ключ представляет собой одну трансформацию:
  • translation (изменение положения, перемещение);
  • вращение (rotation);
  • изменение масштаба (scaling).
Эти ключи представлены в том числе матрицами, которые можно комбинировать (=перемножать), объединяя несколько трансформаций в одну (например для тех случаев, когда 3D-объект двигается + вращается одновременно). Каждый такой ключ содержит временную отметку, в течение которого он активен. Например, если в ключе вращения (rotation key) проставлена отметка time=0, то на временной отметке 0 будет применено данное значение вращения. А если второй ключ вращения активируется на отметке 200, то, исходя из начального и конечного временных ключей вращения, рассчитываются все промежуточные ориентации объекта в процессе анимации. При достижении анимацией отметки 200, значение вращения будет равно второму ключу вращения (second rotation key). Другие виды анимации работают схожим образом.
Закрыть
noteОбрати внимание

Когда мы говорим про время, в нашем случае оно не привязано к какой-либо системе измерения. Игрокодер сам решает, в чём его измерять. Например отрезки времени можно измерять в секундах, прошедших с определённого момента. Временем также может быть прошедшее число кадров. Для упрощения задачи работы со временем его интервалы обычно привязываются к системному времени компьютера, позволяющему отсчитывать равные промежутки времени.


Группы анимаций (Animation sets)

В пределах одного .Х-файла несколько анимаций объединяются в группы или наборы. Каждому набору "приписан" определённый фрейм. Возможна ситуация, когда одному набору назначено более одного ключа анимации. Например фрейм может быть изменён набором ключей вращения (set of rotation-keys) и набором ключей трансляции (set of translation-keys) одновременно. К счастью, 3D-редакторы (чаще всего) позволяют не иметь дело с данными анимаций .Х-файла напрямую.

Анимация по ключевым кадрам (Key Frame animation)

Image
Рис.2 Промежуточные ориентации фреймов автоматический интерполируются, исходя из данных о начальном и конечном положении 3D-объекта

  • Берутся два различных положения объекта (два разных кадра анимации) и интерполируются (плавно переходят) из одного в другой с течением времени.
При этом компьютер автоматически рассчитывает все промежуточные кадры. В результате зритель видит на экране плавное перемещение/вращение 3D-объекта. Данный метод широко распространён в 3D-редакторах для создания CG-анимаций (т.е. создании 3D-видеороликов, например, рекламного характера) и даже программах видеомонтажа.
Закрыть
noteЛюбопытный факт

Метод интерполяции широко применяется в математике. Он представляет собой способ расчёта промежуточных значений между двумя числами, в течение определённого промежутка времени. Например, если от дома до твоей работы 2 километра и ты ежедневно добираешься туда за 30 минут, то по этим данным можно определить пройденную дистанцию на каждом конкретном временном участке по следующей формуле: Distance = (DistanceToWork / TravelTime) * CurrentTime .
Путём несложных вычислений определяем, что, спустя 26 минут после начала пути, ты пройдёшь (2 / 30) * 26 = 1,73 км.

Как ты знаешь, иерархия фреймов формируется по принципу присоединения одних фреймов к другим. Добавим сюда вычисления интерполяции промежуточных кадров и получим плавную анимацию. В данной статье мы применяем технику матричных ключевых кадров (matrix key framing). Благодаря применению матричных объектов из библиотеки D3DX техника матричных ключевых кадров реализуется достаточно просто. Допустим, у нас есть две матрицы: Mat1 и Mat2, представляющие собой начальную (starting) и конечную (ending) матрицы соответственно. Временной отрезок представлен как Length, а текущее время - Time (т.е. промежуток от отметки 0 до Length; См. Рис.2). Интерполированная матрица (interpolated matrix) вычисляется так:
// D3DXMATRIX Mat1, Mat2;
// DWORD Length, Time;

D3DXMATRIX Mat1nt; // Результирующая матрица интерполяции.

// Вычисляем матрицу интерполяции.
Mat1nt = (Mat2 - Mat1) / Length;

// Умножаем на время.
Mat1nt *= Time;

// Прибавляем начальную матрицу и готово!
Matlnt += Mat1;

В результате получаем матрицу, содержащую ориентацию/положение 3D-объекта на любом промежуточном отрезке времени.

Итоги

На первый взгляд урезанная MSVC++2010 Express Edition при грамотном применении оказывается вполне удобной и пригодной для игрокодинга IDE.

Источники


1. Adams J. Programming Role Playing Games with DirectX 8.0. - Premier Press, 2002


Последние изменения страницы Среда 18 / Май, 2022 00:02:31 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++